/*
    Copyright(C) 2010 Ying-Chun Liu(PaulLiu). All rights reserved.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

package org.debian.paulliu.plft;

import java.io.*;
import java.util.*;
import java.net.*;

public class FileChannel implements SessionChannelEventListener{
    Session session;
    AbstractMap<java.nio.ByteBuffer, LocalFile> localFiles;
    byte channelNumber = (byte)(21);
    private Vector<FileChannelEventListener> eventListeners;

    class LocalFile {
	private File file=null;
	private long length=0;
	private java.nio.ByteBuffer hash=null;

	public LocalFile() {
	}

	public void setFile(File file) {
	    this.file = file;
	}
	public File getFile() {
	    return file;
	}
	public void setLength(long length) {
	    this.length = length;
	}
	public long getLength() {
	    return length;
	}
	public void setHash(java.nio.ByteBuffer hash) {
	    this.hash = hash;
	}
	public void setHash(byte[] hash) {
	    java.nio.ByteBuffer hashb=null;
	    hashb = java.nio.ByteBuffer.wrap(hash);
	    setHash(hashb);
	}
	public java.nio.ByteBuffer getHash() {
	    return hash;
	}
    }

    class FileChannelSenderThread extends Thread {
	private int ID;
	private long startpos;
	private LocalFile inputFile;

	public FileChannelSenderThread (LocalFile inputFile, long startpos, int ID) {
	    this.ID = ID;
	    this.startpos = startpos;
	    this.inputFile = inputFile;
	}

	public void run() {
	    FileInputStream fin = null;
	    long progress=0;
	    try {
		fin = new FileInputStream(inputFile.getFile());
	    } catch (Exception e) {
		return;
	    }
	    try {
		if (startpos != 0) {
		    fin.skip(startpos);
		    progress = startpos;
		}
		while (fin.available()>0) {
		    int rlen;
		    byte[] buf=new byte[900];
		    rlen = fin.read(buf);
		    if (rlen<=0) {
			break;
		    }
		    progress += rlen;
		    byte[] sendbuf = new byte[rlen+8+1+5];
		    sendbuf[0] = 'C';
		    sendbuf[1] = 'H';
		    sendbuf[2] = 'A';
		    sendbuf[3] = 'N';
		    sendbuf[4] = 'N';
		    sendbuf[5] = 'E';
		    sendbuf[6] = 'L';
		    sendbuf[7] = ' ';
		    sendbuf[8] = (byte)(channelNumber);
		    sendbuf[9] = 'D';
		    sendbuf[10] = (byte)((ID/256/256/256)%256);
		    sendbuf[11] = (byte)((ID/256/256)%256);
		    sendbuf[12] = (byte)((ID/256)%256);
		    sendbuf[13] = (byte)(ID%256);
		    for (int i=0; i<rlen; i++) {
			sendbuf[i+14] = buf[i];
		    }
		    session.sendPacket(sendbuf);

		    FileChannelEvent fileEvent = new FileChannelEvent(this, FileChannelEvent.FileChannelEventType.PROGRESS);
		    fileEvent.setLocalFile(this.inputFile);
		    fileEvent.setProgress(progress);
		    fileEvent.setDirection(FileChannelEvent.FileChannelEventDirection.UP);
		    processFileChannelEvent(fileEvent);
		}
		fin.close();
		fin=null;
	    } catch (Exception e) {
		return;
	    }
	}
    }

    class FileChannelReceiver implements SessionChannelEventListener {
	private OutputStream fout=null;
	private int ID;
	LocalFile remoteFile;
	long position=0;
	public FileChannelReceiver (int ID, LocalFile remoteFile, OutputStream fout) {
	    this.ID = ID;
	    this.fout = fout;
	    this.remoteFile = remoteFile;
	}
	public void setPosition(long position) {
	    this.position = position;
	}
	public void dataReceived(SessionChannelEvent e) {
	    byte[] data = e.getData();
	    if (data[0] == 'D' && data.length >= 1+4) {
		/* data request */
		/* packet data: 4 byte for id, rest is data */
		int id;
		id = (data[1] & 0xff) * 256*256*256
		    + (data[2] & 0xff) * 256*256
		    + (data[3] & 0xff) * 256
		    + (data[4] & 0xff);
		if (id == this.ID && fout != null) {
		    try {
			fout.write(data, 5, data.length-5);
		    } catch (Exception excep1) {
			try {
			    fout.close();
			} catch (Exception excep2) {
			}
			fout = null;
		    }
		    position += (data.length-5);
		}
		FileChannelEvent fileEvent = new FileChannelEvent(this, FileChannelEvent.FileChannelEventType.PROGRESS);
		fileEvent.setLocalFile(this.remoteFile);
		fileEvent.setProgress(position);
		fileEvent.setDirection(FileChannelEvent.FileChannelEventDirection.DOWN);
		processFileChannelEvent(fileEvent);
		if (position >= remoteFile.getLength()) {
		    try {
			fout.close();
		    } catch (Exception excep1) {
		    }
		    fout = null;
		}
	    }
	}
    }


    public FileChannel(Session session) {
	this.session = session;
	session.addSessionChannelEventListener((int)channelNumber, this);
	localFiles = new java.util.TreeMap<java.nio.ByteBuffer, LocalFile>();
	eventListeners = new Vector<FileChannelEventListener>();
    }

    public void getFile(LocalFile remoteFile, File outputFile) {
	/* open outputFile */
	FileOutputStream fout = null;
	try {
	    fout = new FileOutputStream(outputFile);
	} catch (Exception e) {
	    return;
	}

	/* register data handler */
	int ID;
	int startpos=0;
	ID = org.debian.paulliu.utils.Counter.getInstanceByName("plft").getValue();
	SessionChannelEventListener fReceiver = new FileChannelReceiver(ID, remoteFile, fout);
	session.addSessionChannelEventListener(channelNumber, fReceiver);
	
	/* send out the get command */
	byte[] sendbuf = new byte[8+1+1+8+4+20];
	sendbuf[0] = 'C';
	sendbuf[1] = 'H';
	sendbuf[2] = 'A';
	sendbuf[3] = 'N';
	sendbuf[4] = 'N';
	sendbuf[5] = 'E';
	sendbuf[6] = 'L';
	sendbuf[7] = ' ';
	sendbuf[8] = (byte)(channelNumber);
	sendbuf[9] = 'G';
	sendbuf[10] = (byte)((startpos/256/256/256/256/256/256/256)%256);
	sendbuf[11] = (byte)((startpos/256/256/256/256/256/256)%256);
	sendbuf[12] = (byte)((startpos/256/256/256/256/256)%256);
	sendbuf[13] = (byte)((startpos/256/256/256/256)%256);
	sendbuf[14] = (byte)((startpos/256/256/256)%256);
	sendbuf[15] = (byte)((startpos/256/256)%256);
	sendbuf[16] = (byte)((startpos/256)%256);
	sendbuf[17] = (byte)(startpos%256);
	sendbuf[18] = (byte)((ID/256/256/256)%256);
	sendbuf[19] = (byte)((ID/256/256)%256);
	sendbuf[20] = (byte)((ID/256)%256);
	sendbuf[21] = (byte)(ID%256);
	byte[] remoteFileHash = null;
	remoteFileHash = remoteFile.getHash().array();
	for (int i=0; remoteFileHash != null && i < remoteFileHash.length && i<20; i++) {
	    sendbuf[i+22] = remoteFileHash[i];
	}
	session.sendPacket(sendbuf);
    }

    public void putFile(File file) {
	boolean result1 = false;
	byte[] hash=null;
	java.security.MessageDigest md=null;
	long fileSize=0;
	byte[] filename=null;
	try {
	    result1 = file.exists();
	} catch (Exception e) {
	}
	if (!result1) {
	    return;
	}
	try {
	    md = java.security.MessageDigest.getInstance("SHA-512");
	} catch (Exception e) {
	    return;
	}
	FileInputStream fin = null;
	try {
	    fin = new FileInputStream(file);
	} catch (Exception e) {
	    return;
	}
	try {
	    while (fin.available()>0) {
		int rlen;
		byte[] buf=new byte[1024];
		rlen = fin.read(buf);
		if (rlen<=0) {
		    break;
		}
		fileSize += (long)(rlen);
		md.update(buf,0,rlen);
	    }
	    fin.close();
	    fin=null;
	} catch (Exception e) {
	}
	hash = md.digest();
	hash = java.util.Arrays.copyOfRange(hash, 0, 20);
	LocalFile localfile = new LocalFile();
	localfile.setFile(file);
	localfile.setLength(fileSize);
	localfile.setHash(hash);
	localFiles.put(localfile.getHash(), localfile);

	try {
	    filename = file.getName().getBytes("UTF-8");
	} catch (Exception e) {
	    return;
	}

	byte[] sendbuf = new byte[filename.length+8+1+1+8+20];
	sendbuf[0] = 'C';
	sendbuf[1] = 'H';
	sendbuf[2] = 'A';
	sendbuf[3] = 'N';
	sendbuf[4] = 'N';
	sendbuf[5] = 'E';
	sendbuf[6] = 'L';
	sendbuf[7] = ' ';
	sendbuf[8] = (byte)(channelNumber);
	sendbuf[9] = 'P';
	sendbuf[10] = (byte)((fileSize/256/256/256/256/256/256/256)%256);
	sendbuf[11] = (byte)((fileSize/256/256/256/256/256/256)%256);
	sendbuf[12] = (byte)((fileSize/256/256/256/256/256)%256);
	sendbuf[13] = (byte)((fileSize/256/256/256/256)%256);
	sendbuf[14] = (byte)((fileSize/256/256/256)%256);
	sendbuf[15] = (byte)((fileSize/256/256)%256);
	sendbuf[16] = (byte)((fileSize/256)%256);
	sendbuf[17] = (byte)(fileSize%256);
	for (int i=0; i<20; i++) {
	    sendbuf[18+i] = hash[i];
	}
	for (int i=0; i<filename.length; i++) {
	    sendbuf[38+i] = filename[i];
	}
	session.sendPacket(sendbuf);
    }

    public void dataReceived(SessionChannelEvent e) {
	byte[] data = e.getData();
	if (data[0] == 'P' && data.length >= 20+8+1) {
	    /* put file request */
	    /* packet data: 8 byte for size, 20 bytes for hash, rest is filename in UTF-8 */
	    long length;
	    byte[] hash = new byte[20];
	    String filename = null;
	    length = (data[1] & 0xff) * 256*256*256*256*256*256*256 
		+ (data[2] & 0xff) * 256*256*256*256*256*256
		+ (data[3] & 0xff) * 256*256*256*256*256
		+ (data[4] & 0xff) * 256*256*256*256
		+ (data[5] & 0xff) * 256*256*256
		+ (data[6] & 0xff) * 256*256
		+ (data[7] & 0xff) * 256
		+ (data[8] & 0xff);
	    hash = java.util.Arrays.copyOfRange(data, 9, 9+20);
	    try {
		filename = new String(data, 29, data.length-29, "utf-8");
	    } catch (Exception e1) {
		filename = null;
	    }
	    java.util.logging.Logger.getLogger("org.debian.paulliu").log(java.util.logging.Level.INFO, "remote wants to put file: " + filename + " Size=" + Long.toString(length));

	    /* length, hash, filename ready */

	    FileChannelEvent fileEvent = new FileChannelEvent(this, FileChannelEvent.FileChannelEventType.PUT);
	    LocalFile rfile = new LocalFile();
	    rfile.setFile(new File(filename));
	    rfile.setLength(length);
	    rfile.setHash(hash);
	    fileEvent.setLocalFile(rfile);
	    processFileChannelEvent(fileEvent);
	} else if (data[0] == 'G' && data.length >= 8+4+20) {
	    /* get file */
	    /* packet data: 8 byte for startpos, 4 byte for ID, 20 byte for hash */
	    long startpos=0;
	    int id=0;
	    byte[] hash = new byte[20];
	    startpos = (data[1] & 0xff) * 256*256*256*256*256*256*256 
		+ (data[2] & 0xff) * 256*256*256*256*256*256
		+ (data[3] & 0xff) * 256*256*256*256*256
		+ (data[4] & 0xff) * 256*256*256*256
		+ (data[5] & 0xff) * 256*256*256
		+ (data[6] & 0xff) * 256*256
		+ (data[7] & 0xff) * 256
		+ (data[8] & 0xff);
	    id = (data[9] & 0xff) * 256*256*256
		+ (data[10] & 0xff) * 256*256
		+ (data[11] & 0xff) * 256
		+ (data[12] & 0xff);
	    hash = java.util.Arrays.copyOfRange(data, 13, 13+20);
	    
	    /* startpos, ID, hash ready */
	    java.nio.ByteBuffer hashb = java.nio.ByteBuffer.wrap(hash);
	    LocalFile file = localFiles.get(hashb);
	    Thread senderThread = new FileChannelSenderThread(file,startpos, id );
	    senderThread.start();
	}
    }

    /**
     * Adds the specified event listener to receive events from this FileChannel component. If l is null, no exception is thrown and no action is performed.
     * @param l the text event listener
     */
    public void addFileChannelEventListener(FileChannelEventListener l) {
	if (l == null) {
	    return;
	}
	eventListeners.add(l);
    }

    /**
     * Processes text events occurring on this text component by dispatching them to any registered SessionTextEventListener objects. 
     * @param e the text event
     */
    protected void processFileChannelEvent (FileChannelEvent e)
    {
	for (int i=0; i<eventListeners.size(); i++) {
	    eventListeners.elementAt(i).receivedFileChannelEvent(e);
	}
    }

}
